<?php
/**
 * crcl/base
 *
 * Copyright 2013 CRCL - André Sabosch - info@crcl.de
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace crcl\base\validator\container;

interface IMysqlIdentifier
{
}

class ContainerMysqlIdentifier extends Base implements IMysqlIdentifier
{
  public function test($value)
  {
    $this->checkEmpty($value);
    $this->checkTrim($value);
    $this->checkQuotes($value);
    $this->checkCharacters($value);
    $this->checkNumeric($value);
    $this->checkStart($value);
    $this->checkLength($value);
    $this->checkReserved($value);

    return \TRUE;
  }

  public function is($value)
  {
    try
    {
      $this->checkEmpty($value);
      $this->checkTrim($value);
      $this->checkQuotes($value);
      $this->checkCharacters($value);
      $this->checkNumeric($value);
      $this->checkStart($value);
      $this->checkLength($value);
      $this->checkReserved($value);
    }
    catch (Exception $e)
    {
      return \FALSE;
    }
    return \TRUE;
  }

  /** PRIVATE *************************************************************************************/

  const BACKTICK = '`';
  private $aReserved = array(
    'accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc', 'asensitive', 'before',
    'between', 'bigint', 'binary', 'blob', 'both', 'by', 'call', 'cascade', 'case', 'change',
    'char', 'character', 'check', 'collate', 'column', 'condition', 'constraint', 'continue',
    'convert', 'create', 'cross', 'current_date', 'current_time', 'current_timestamp',
    'current_user', 'cursor', 'database', 'databases', 'day_hour', 'day_microsecond', 'day_minute',
    'day_second', 'dec', 'decimal', 'declare', 'default', 'delayed', 'delete', 'desc', 'describe',
    'deterministic', 'distinct', 'distinctrow', 'div', 'double', 'drop', 'dual', 'each', 'else',
    'elseif', 'enclosed', 'escaped', 'exists', 'exit', 'explain', 'false', 'fetch', 'float',
    'float4', 'float8', 'for', 'force', 'foreign', 'from', 'fulltext', 'grant', 'group', 'having',
    'high_priority', 'hour_microsecond', 'hour_minute', 'hour_second', 'if', 'ignore', 'in',
    'index', 'infile', 'inner', 'inout', 'insensitive', 'insert', 'int', 'int1', 'int2', 'int3',
    'int4', 'int8', 'integer', 'interval', 'into', 'is', 'iterate', 'join', 'key', 'keys', 'kill',
    'leading', 'leave', 'left', 'like', 'limit', 'linear', 'lines', 'load', 'localtime',
    'localtimestamp', 'lock', 'long', 'longblob', 'longtext', 'loop', 'low_priority',
    'master_ssl_verify_server_cert', 'match', 'maxvalue', 'mediumblob', 'mediumint', 'mediumtext',
    'middleint', 'minute_microsecond', 'minute_second	mod', 'modifies', 'natural', 'not',
    'no_write_to_binlog', 'null', 'numeric', 'on', 'optimize', 'option', 'optionally', 'or',
    'order', 'out', 'outer', 'outfile', 'precision', 'primary', 'procedure', 'purge', 'range',
    'read', 'reads', 'read_write', 'real', 'references', 'regexp', 'release', 'rename', 'repeat',
    'replace', 'require', 'resignal', 'restrict', 'return', 'revoke', 'right', 'rlike', 'schema',
    'schemas', 'second_microsecond', 'select', 'sensitive', 'separator', 'set', 'show', 'signal',
    'smallint', 'spatial', 'specific', 'sql', 'sqlexception', 'sqlstate', 'sqlwarning',
    'sql_big_result', 'sql_calc_found_rows', 'sql_small_result', 'ssl', 'starting', 'straight_join',
    'table', 'terminated', 'then', 'tinyblob', 'tinyint', 'tinytext', 'to', 'trailing', 'trigger',
    'true', 'undo', 'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use', 'using',
    'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary', 'varchar', 'varcharacter',
    'varying', 'when', 'where', 'while', 'with', 'write', 'xor', 'year_month', 'zerofill',
    'general', 'ignore_server_ids', 'master_heartbeat_period', 'maxvalue', 'resignal', 'signal',
    'slow');

  private function checkEmpty($value)
  {
    if (empty($value))
    {
      throw new Exception('Invalid identifier: empty', Exception::ERROR_MYSQLIDENTIFIER_EMPTY,
        array('identifier' => $value));
    }
  }

  private function checkTrim($value)
  {
    $sCheck = \trim($value);
    if ($sCheck !== $value)
    {
      throw new Exception('Invalid identifier: trim', Exception::ERROR_MYSQLIDENTIFIER_TRIM,
        array('idenfier' => $value));
    }
  }

  private function checkQuotes($value)
  {
    $iCount = \substr_count($value, self::BACKTICK);
    if (($iCount !== 0) && ($iCount !== 2) && ($iCount !== 4))
    {
      throw new Exception('Invalid identifier: quotes quantity',
        Exception::ERROR_MYSQLIDENTIFIER_QUOTES,
        array('identifier' => $value));
    }
    $bOpen = \FALSE;
    $bClose = \TRUE;
    $iMax = \strlen($value);
    for ($i=0; $i<$iMax; $i++)
    {
      if ($value[$i] == self::BACKTICK)
      {
        $bOpen = !$bOpen;
        $bClose = !$bClose;

        if (($i !== 0) && ($i !== $iMax-1))
        {
          if (($value[$i-1] !== '.') && ($value[$i+1] !== '.'))
          {
            throw new Exception('Invalid identifier: quotes',
              Exception::ERROR_MYSQLIDENTIFIER_QUOTES,
              array('identifier' => $value));
          }
        }
      }
      if ($value[$i] == '.')
      {
        if ($bOpen)
        {
          throw new Exception('Invalid identifier: quotes span',
            Exception::ERROR_MYSQLIDENTIFIER_QUOTES,
            array('identifier' => $value));
        }
      }
    }
  }

  private function checkCharacters($value)
  {
    $bMatch = preg_match('/[^A-Za-z0-9$_.`]/', $value);
    if ($bMatch)
    {
      throw new Exception('Invalid identifier: characters',
        Exception::ERROR_MYSQLIDENTIFIER_CHARACTERS,
        array('identifier' => $value));
    }
    if (\substr_count($value, '.') >= 2)
    {
      throw new Exception('Invalid identifier: characters / dot.',
        Exception::ERROR_MYSQLIDENTIFIER_CHARACTERS,
        array('identifier' => $value));
    }
    if (($value[0] == '.') || ($value[\strlen($value)-1] == '.'))
    {
      throw new Exception('Invalid identifier: characters / dot.',
        Exception::ERROR_MYSQLIDENTIFIER_CHARACTERS,
        array('identifier' => $value));
    }
  }

  private function checkNumeric($value)
  {
    if (\is_numeric($value)) //will not trigger if quotet
    {
      throw new Exception('Invalid identifier: numeric without quotes',
        Exception::ERROR_MYSQLIDENTIFIER_NUMERIC,
        array('identifier' => $value));
    }
  }

  private function checkStart($value)
  {
    $value = \crcl\base\String::factory($value)->doNotStartWith(self::BACKTICK)->toString();
    if ($value[1] == 'e')
    {
      if (\is_numeric($value[0]))
      {
        throw new Exception('Invalid identifier: Ne start.',
          Exception::ERROR_MYSQLIDENTIFIER_START ,
          array('identifier' => $value));
      }
    }
  }

  //64, except alias (256) and compound statement label (16). we did ignore that....
  private function checkLength($value)
  {
    $iLength = \strlen($value);
    if ($iLength > 64)
    {
      throw new Exception('Invalid identifier: length',
        Exception::ERROR_MYSQLIDENTIFIER_LENGTH,
        array('identifier' => $value, 'length' => $iLength));
    }
  }

  private function checkReserved($value)
  {
    //do not respect backticks, because reserved words in backticks are ok
    $aValues = \explode('.', $value);
    foreach ($aValues as $sValue)
    {
      if (\in_array(\strtolower($sValue), $this->aReserved))
      {
        throw new Exception('Invalid identifier: reserved',
          Exception::ERROR_MYSQLIDENTIFIER_RESERVED,
          array('identifier' => $value, 'reserved' => $sValue));
      }
    }
  }
}